#region License

/* Copyright (c) 2008, Dusty Candland
 * 
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * * Neither the name of Dusty Candland nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#endregion

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net;
using System.Xml;

namespace NS3Library.Amazon
{
    public class AwsS3Service : IS3Service
    {
        private readonly AwsS3ServiceOptions _options = AwsS3ServiceOptions.None;
        private readonly RequestSigner _signer;
        private readonly UrlBuilder _urlBuilder;
        private AwsStatus _lastStatus;

        public AwsS3Service(string awsKey, string awsSecret)
            : this(awsKey, awsSecret, AwsS3ServiceOptions.None)
        {
        }

        public AwsS3Service(string awsKey, string awsSecret, AwsS3ServiceOptions options)
        {
            _options = options;
            _signer = new RequestSigner(awsKey, awsSecret);
            _urlBuilder = new UrlBuilder(AwsUrlStyle.Host, _options.Equals(AwsS3ServiceOptions.UseSsl));
        }

        public AwsStatus LastStatus
        {
            get { return _lastStatus; }
        }

        public IS3Bucket Put(IS3Bucket s3Bucket)
        {
            EnsureBucket(s3Bucket);

            HttpWebRequest request = CreateRequest(KnownHttpVerbNames.Put, _urlBuilder.Build(s3Bucket.Name),
                                                   s3Bucket.Headers());
            var awsBucket = s3Bucket as AwsBucket;
            if (awsBucket != null)
            {
                request.ContentLength = awsBucket.ContentLength;
                if (awsBucket.HasContent)
                    WriteContent(request, awsBucket.Content);
            }
            HttpWebResponse response = GetResponse(request);
            AwsStatus status = GetStatus(response);
            if (status.HttpStatus != HttpStatusCode.OK)
                throw new S3ServiceException("Put bucket failed " + status);
            return new AwsBucket(s3Bucket, status);
        }

        public IS3Object Put(IS3Object s3Object)
        {
            AwsObject awsObject = EnsureObject(s3Object);

            HttpWebRequest request =
                CreateRequest(KnownHttpVerbNames.Put, _urlBuilder.Build(awsObject.BucketName, awsObject.Key),
                              awsObject.Headers());
            request.ContentLength = awsObject.Content.Length;
            if (awsObject.Content.Length > 0)
                WriteContent(request, awsObject.Content);

            HttpWebResponse response = GetResponse(request);
            AwsStatus status = GetStatus(response);
            if (status.HttpStatus != HttpStatusCode.OK)
                throw new S3ServiceException("Put object failed " + status);
            return new AwsObject(awsObject, status);
        }

        public IEnumerable<IS3Bucket> Get()
        {
            HttpWebRequest request = CreateRequest(KnownHttpVerbNames.Get, _urlBuilder.Build(), null);
            HttpWebResponse response = GetResponse(request);
            XmlDocument xml = AwsS3Parser.Xml(response);
            GetStatus(response, xml);

            if (response.StatusCode == HttpStatusCode.OK)
                return AwsS3Parser.GetBucketList(xml);
            return new List<IS3Bucket>();
        }

        public IS3Bucket Get(string bucketName)
        {
            var s3Bucket = new AwsBucket(bucketName);

            HttpWebRequest request = CreateRequest(KnownHttpVerbNames.Get, _urlBuilder.Build(s3Bucket.Name),
                                                   s3Bucket.Headers());
            HttpWebResponse response = GetResponse(request);
            XmlDocument xml = AwsS3Parser.Xml(response);
            AwsStatus status = GetStatus(response, xml);

            if (status.HttpStatus == HttpStatusCode.OK)
                return new AwsBucket(AwsS3Parser.GetBucket(xml), status);
            return null;
        }

        public IS3Bucket Get(string bucket, string prefix, int maxKeys, string marker)
        {
            throw new NotImplementedException();
        }

        public IS3Bucket Get(string bucket, string prefix, int maxKeys, string marker, string delimiter)
        {
            throw new NotImplementedException();
        }

        public IS3Object Get(IS3Bucket bucket, string key)
        {
            return Get(bucket.Name, key);
        }

        public IS3Object Get(string bucketName, string key)
        {
            HttpWebRequest request =
                CreateRequest(KnownHttpVerbNames.Get, _urlBuilder.Build(bucketName, key), null);
            HttpWebResponse response = GetResponse(request);
            AwsStatus status = GetStatus(response);
            if (status.HttpStatus == HttpStatusCode.OK)
                return AwsS3Parser.GetDataObject(bucketName, key, status, response.Headers, response.GetResponseStream());
            return null;
        }

        public void DeleteBucket(IS3Bucket s3Bucket)
        {
            EnsureBucket(s3Bucket);
            DeleteBucket(s3Bucket.Name);
        }

        public void DeleteBucket(string bucketName)
        {
            HttpWebRequest request =
                CreateRequest(KnownHttpVerbNames.Delete, _urlBuilder.Build(bucketName), null);
            HttpWebResponse response = GetResponse(request);
            AwsStatus status = GetStatus(response);
            DeleteSuccessful(status);
        }

        public void DeleteObject(IS3Object s3Object)
        {
            DeleteObject(s3Object.BucketName, s3Object.Key);
        }

        public void DeleteObject(string bucketName, string key)
        {
            HttpWebRequest request =
                CreateRequest(KnownHttpVerbNames.Delete, _urlBuilder.Build(bucketName, key), null);
            HttpWebResponse response = GetResponse(request);
            AwsStatus status = GetStatus(response);
            DeleteSuccessful(status);
        }

        private static void DeleteSuccessful(AwsStatus status)
        {
            if (status.HttpStatus != HttpStatusCode.NoContent && status.HttpStatus != HttpStatusCode.NotFound)
                throw new S3ServiceException("Delete Failed");
        }

        private AwsStatus GetStatus(HttpWebResponse response)
        {
            return GetStatus(response, AwsS3Parser.Xml(response));
        }

        private AwsStatus GetStatus(HttpWebResponse response, XmlDocument xml)
        {
            AwsStatus status = AwsS3Parser.GetStatus(
                response.StatusCode,
                response.Headers,
                xml);
            _lastStatus = status;
            return status;
        }

        private static AwsObject EnsureObject(IS3Object s3Object)
        {
            var awsObject = s3Object as AwsObject;
            if (s3Object == null)
                throw new ArgumentNullException("s3Object", Resources.ExceptionAwsObjectArgIsNull);
            return awsObject;
        }

        private static void EnsureBucket(IS3Bucket awsBucket)
        {
            if (awsBucket == null)
                throw new ArgumentNullException("awsBucket", Resources.ExceptionAwsBucketArgIsNull);
        }

        private static HttpWebResponse GetResponse(WebRequest request)
        {
            HttpWebResponse response;
            try
            {
                response = request.GetResponse() as HttpWebResponse;
            }
            catch (WebException we)
            {
                return we.Response as HttpWebResponse;
            }
            return response;
        }

        private HttpWebRequest CreateRequest(string verb, string url, NameValueCollection headers)
        {
            var request = WebRequest.Create(url) as HttpWebRequest;
            if (request == null)
                throw new ArgumentOutOfRangeException("url", url, Resources.ExceptionInvalidUrlArg);

            request.Method = verb.ToUpper(CultureInfo.InvariantCulture);

            if (headers != null)
                request.Headers.Add(headers);

            request.Headers["x-amz-date"] = DateTime.Now.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture);
            request.Headers[KnownHttpHeaderNames.Authorization] = _signer.Sign(verb, url, request.Headers);

            return request;
        }

        private static void WriteContent(WebRequest request, byte[] content)
        {
            Stream stream = request.GetRequestStream();
            stream.Write(content, 0, content.Length);
            stream.Close();
        }

        public IS3Bucket CreateIS3Bucket(string name)
        {
            return new AwsBucket(name);
        }

        public IS3Bucket CreateIS3Bucket(string name, S3LocationConstraint locationConstraint, S3Acl acl)
        {
            return new AwsBucket(name, locationConstraint, acl);
        }

        public IS3Object CreateIS3Object(string bucketName, string key, byte[] content, NameValueCollection metadata)
        {
            return new AwsObject(bucketName, key, content, metadata);
        }
    }
}